//
//  LTDownloadManager.m
//  Total
//
//  Created by xin on 2017/7/24.
//  Copyright © 2017年 elephants. All rights reserved.
//

#import "LTDownloadManager.h"
#import "LTDownloadItem.h"

@interface LTDownloadManager() <NSURLSessionDelegate>

/**
 后台线程ID,内部初始化，外部不能被修改
 */
@property (nonatomic, copy, nonnull) NSString *backgroundSessionIdentifier;
/**
 最大文件下载链接数
 */
@property (nonatomic, assign) NSInteger maxConcurrentFileDownloadsCount;
/**
 文件下载代理，给下载类使用
 */
@property (nonatomic, weak, nullable) NSObject<LTDownloadDelegate>* fileDownloadDelegate;
/**
 活跃的下载对象字典
 */
@property (nonatomic, strong, nonnull) NSMutableDictionary <NSNumber *,LTDownloadItem *> *activeDownloadsDictionary;
/**
 等待下载的数据数组
 */
@property (nonatomic, strong, nonnull) NSMutableArray <NSDictionary <NSString *,NSObject *> *> *waitingDownloadsArray;
/**
 后台下载完成的block回调，从application: handleEventsForBackgroundURLSession: completionHandler:中取到block并保存，这样在下载的session都初始化后，调用此block可以在session的代理方法中获得文件下载的信息进行处理。
 */
@property (nonatomic, copy, nullable) LTBackgroundSessionCompletionHandlerBlock backgroundSessionCompletionHandlerBlock;

/**
 下载会话对象
 */
@property (nonatomic, strong, nonnull) NSURLSession * backgroundSession;

@end

@implementation LTDownloadManager

#pragma mark - Initialization

- (nullable instancetype)initWithDelegate:(nonnull NSObject<LTDownloadDelegate>*)delegate
{
    return [self initWithDelegate:delegate maxConcurrentDownloads:-1];
}

- (nullable instancetype)initWithDelegate:(nonnull NSObject<LTDownloadDelegate>*)delegate maxConcurrentDownloads:(NSInteger)maxConcurrentFileDownloadsCount
{
    NSString * aBackgroundDownloadSessionIdentifier = [NSString stringWithFormat:@"%@.LTDownload",[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]];
    return [self initWithDelegate:delegate maxConcurrentDownloads:maxConcurrentFileDownloadsCount backgroundSessionIdentifier:aBackgroundDownloadSessionIdentifier];
}

- (nullable instancetype)initWithDelegate:(nonnull NSObject<LTDownloadDelegate>*)delegate maxConcurrentDownloads:(NSInteger)maxConcurrentFileDownloadsCount backgroundSessionIdentifier:(nonnull NSString *)backgroundSessionIdentifier
{
    self = [super init];
    if (self) {
        
        self.backgroundSessionIdentifier = backgroundSessionIdentifier;
        self.maxConcurrentFileDownloadsCount = -1;
        if(self.maxConcurrentFileDownloadsCount > 0)
        {
            self.maxConcurrentFileDownloadsCount = maxConcurrentFileDownloadsCount;
        }
        
        self.fileDownloadDelegate = delegate;
        
        NSURLSessionConfiguration * backgroundConfiguration = nil;
        if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_7_1) {
            backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:self.backgroundSessionIdentifier];
        }else{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
            backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:self.backgroundSessionIdentifier];
#pragma GCC diagnostic pop
        }
        
        if ([self.fileDownloadDelegate respondsToSelector:@selector(customizeBackgroundSessionConfiguration:)]) {
            [self.fileDownloadDelegate customizeBackgroundSessionConfiguration:backgroundConfiguration];
        }
        
        self.backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfiguration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
        
    }
    return self;
}

- (void)setupWithCompletion:(nullable void (^)(void))aSetupCompletionBlock
{
    [self.backgroundSession getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> * _Nonnull dataTasks, NSArray<NSURLSessionUploadTask *> * _Nonnull uploadTasks, NSArray<NSURLSessionDownloadTask *> * _Nonnull downloadTasks) {
        
        for (NSURLSessionDownloadTask * aDownloadTask in downloadTasks) {
            NSString * aDownloadToken = [aDownloadTask.taskDescription copy];
            if (aDownloadToken) {
                NSProgress *aRootProgress = nil;
                if ([self.fileDownloadDelegate respondsToSelector:@selector(rootProgress)]) {
                    aRootProgress = [self.fileDownloadDelegate rootProgress];
                }
                aRootProgress.totalUnitCount++;
                [aRootProgress becomeCurrentWithPendingUnitCount:1];
                LTDownloadItem * aDownloadItem = [[LTDownloadItem alloc] initWithDownloadToken:aDownloadToken sessionDownloadTask:aDownloadTask];
                [aRootProgress resignCurrent];
                
                if (aDownloadItem) {
                    [self.activeDownloadsDictionary setObject:aDownloadItem forKey:@(aDownloadTask.taskIdentifier)];
                    
                    NSString * aDownloadToken = [aDownloadItem.downloadToken copy];
                    [aDownloadItem.progress setPausingHandler:^{
                        dispatch_async(dispatch_get_main_queue(), ^{
                            [self pauseDownloadWithIdentifier:aDownloadToken];
                        });
                    }];
                    [aDownloadItem.progress setCancellationHandler:^{
                        dispatch_async(dispatch_get_main_queue(), ^{
                            [self cancelDownloadWithIdentifier:aDownloadToken];
                        });
                    }];
                    //ios9开始使用
                    if (floor(NSFoundationVersionNumber_iOS_8_4)) {
                        [aDownloadItem.progress setResumingHandler:^{
                            dispatch_async(dispatch_get_main_queue(), ^{
                                [self resumeDownloadWithIdentifier:aDownloadToken];
                            });
                        }];
                    }
                }
                [self.fileDownloadDelegate incrementNetworkActivityIndicatorActivityCount];
            }
            else
            {
                NSLog(@"ERR: Missing task description (%@, %d)", [NSString stringWithUTF8String:__FILE__].lastPathComponent, __LINE__);
            }
        }
        if(aSetupCompletionBlock){
            aSetupCompletionBlock();
        }
    }];
}

- (void)dealloc
{
    [self.backgroundSession finishTasksAndInvalidate];
}

#pragma mark - Download

- (void)startDownloadWithIdentifier:(nonnull NSString *)aDownloadIdentifier fromRemoteURL:(nonnull NSURL *)aRemoteURL
{
    [self startDownloadWithDownloadToken:aDownloadIdentifier fromRemoteURL:aRemoteURL usingResumeData:nil];
}

- (void)startDownloadWithIdentifier:(nonnull NSString *)aDownloadIdentifier usingResumeData:(nonnull NSData *)aResumeData
{
    [self startDownloadWithDownloadToken:aDownloadIdentifier fromRemoteURL:nil usingResumeData:aResumeData];
}

- (void)startDownloadWithDownloadToken:(nonnull NSString *)aDownloadToken fromRemoteURL:(nullable NSURL *)aRemoteURL usingResumeData:(nullable NSData *)aResumeData
{
    NSUInteger aDownloadID = 0;
    if ((self.maxConcurrentFileDownloadsCount == -1) || ((NSInteger)self.activeDownloadsDictionary.count < self.maxConcurrentFileDownloadsCount))
    {
        NSURLSessionDownloadTask * aDownloadTask = nil;
        LTDownloadItem * aDownloadItem = nil;
        NSProgress * aRootProgress = nil;
        if ([self.fileDownloadDelegate respondsToSelector:@selector(rootProgress)])
        {
            aRootProgress = [self.fileDownloadDelegate rootProgress];
        }
        
        // NSURLSessionDownloadTask init
        if (aResumeData)
        {
            aDownloadTask = [self.backgroundSession downloadTaskWithResumeData:aResumeData];
        }
        else if (aRemoteURL)
        {
            aDownloadTask = [self.backgroundSession downloadTaskWithURL:aRemoteURL];
        }
        aDownloadID = aDownloadTask.taskIdentifier;
        aDownloadTask.taskDescription = aDownloadToken;
        
        aRootProgress.totalUnitCount++;
        [aRootProgress becomeCurrentWithPendingUnitCount:1];
        aDownloadItem = [[LTDownloadItem alloc] initWithDownloadToken:aDownloadToken sessionDownloadTask:aDownloadTask];
        if (aResumeData)
        {
            aDownloadItem.resumedFileSizeInBytes = aResumeData.length;
            aDownloadItem.downloadStartDate = [NSDate date];
            aDownloadItem.bytesPerSecondSpeed = 0;
        }
        [aRootProgress resignCurrent];
        
        if (aDownloadItem)
        {
            [self.activeDownloadsDictionary setObject:aDownloadItem forKey:@(aDownloadID)];
            NSString *aDownloadToken = [aDownloadItem.downloadToken copy];
            [aDownloadItem.progress setPausingHandler:^{
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self pauseDownloadWithIdentifier:aDownloadToken];
                });
            }];
            [aDownloadItem.progress setCancellationHandler:^{
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self cancelDownloadWithIdentifier:aDownloadToken];
                });
            }];
            if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_8_4)
            {
                [aDownloadItem.progress setResumingHandler:^{
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [self resumeDownloadWithIdentifier:aDownloadToken];
                    });
                }];
            }
            
            [aDownloadTask resume];
        }
        else
        {
            NSLog(@"ERROR: No download item (%@, %d)",[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__);
        }
        
    }
    else
    {
        NSMutableDictionary * aWaitingDownloadDict = [NSMutableDictionary dictionary];
        [aWaitingDownloadDict setObject:aDownloadToken forKey:@"downloadToken"];
        if (aResumeData) {
            [aWaitingDownloadDict setObject:aResumeData forKey:@"resumeData"];
        }
        else if(aRemoteURL)
        {
            [aWaitingDownloadDict setObject:aRemoteURL forKey:@"remoteURL"];
        }
        [self.waitingDownloadsArray addObject:aWaitingDownloadDict];
    }
}

- (void)resumeDownloadWithIdentifier:(nonnull NSString *)aDownloadIndentifier
{
    BOOL isDownloading = [self isDownloadingIdentifier:aDownloadIndentifier];
    if (isDownloading == NO)
    {
        if ([self.fileDownloadDelegate respondsToSelector:@selector(resumeDownloadWithIdentifier:)]) {
            [self.fileDownloadDelegate resumeDownloadWithIdentifier:aDownloadIndentifier];
        }
        else
        {
            NSLog(@"ERROR: Resume data called without implement (%@, %d)",[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__);
        }
    }
}

#pragma mark - Download Stop
- (void)pauseDownloadWithIdentifier:(nonnull NSString *)aDownloadIndentifier
{
    BOOL isDownloading = [self isDownloadingIdentifier:aDownloadIndentifier];
    if (isDownloading)
    {
        [self pauseDownloadWithIdentifier:aDownloadIndentifier resumeDataBlock:^(NSData * _Nullable resumeData) {
            if ([self.fileDownloadDelegate respondsToSelector:@selector(downloadPausedWithIdentifier:resumaData:)])
            {
                if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_8_4)
                {
                    //resume data is managed by the system and used when calling resume with NSProgress
                    resumeData = nil;
                }
                [self.fileDownloadDelegate downloadPausedWithIdentifier:aDownloadIndentifier resumaData:resumeData];
            }
        }];
    }
}

- (void)pauseDownloadWithIdentifier:(nonnull NSString *)aDownloadIndentifier resumeDataBlock:(nullable LTDownloadManagerPauseResumaDataBlock)aResumeDataBlock
{
    NSInteger aDownloadID = [self downloadIDForActiveDownloadToken:aDownloadIndentifier];
    if (aDownloadID > -1)
    {
        
        [self pauseDownloadWtihDownloadID:aDownloadID resumeDataBlock:aResumeDataBlock];
    }
    else
    {
        NSInteger aFoundIndex = -1;
        for (NSUInteger anIndex = 0; anIndex < self.waitingDownloadsArray.count; anIndex++)
        {
            NSDictionary *aWaitingDownloadDict = self.waitingDownloadsArray[anIndex];
            if ([aWaitingDownloadDict[@"downloadToken"] isEqualToString:aDownloadIndentifier])
            {
                aFoundIndex = anIndex;
                break;
            }
            aFoundIndex++;
        }
        if (aFoundIndex > -1)
        {
            [self.waitingDownloadsArray removeObjectAtIndex:aFoundIndex];
        }
    }
    
}

- (void)pauseDownloadWtihDownloadID:(NSInteger)aDownloadID resumeDataBlock:(nullable LTDownloadManagerPauseResumaDataBlock)aResumeDataBlock
{
    LTDownloadItem *aDownloadItem = [self.activeDownloadsDictionary objectForKey:@(aDownloadID)];
    if (aDownloadItem)
    {
        NSURLSessionDownloadTask * aDownloadTask = aDownloadItem.sessionDownloadTask;
        if (aDownloadTask)
        {
            [aDownloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
                aResumeDataBlock(resumeData);
            }];
        }
        else
        {
            [aDownloadTask cancel];
        }
        //取消任务后 NSURLSessionTaskDelegate 会调用
        //URLSession:task:didCompleteWithError:
    }
    else
    {
        NSLog(@"INFO: NSURLSessionDownloadTask cancelled (task not found): %@(%@, %d)",aDownloadItem.downloadToken,[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__);
        NSError * aPauseError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil];
        [self handleDownloadWithError:aPauseError downloadItem:aDownloadItem downloadID:aDownloadID resumaData:nil];
    }
}

- (void)cancelDownloadWithIdentifier:(nonnull NSString *)aDownloadIndentifier
{
    NSInteger aDownloadID = [self downloadIDForActiveDownloadToken:aDownloadIndentifier];
    if (aDownloadID > -1) {
        [self cancelDownloadWithDownloadID:aDownloadID];
    }
    else
    {
        NSInteger aFoundIndex = -1;
        for (NSUInteger anIndex = 0; anIndex < self.waitingDownloadsArray.count; anIndex++)
        {
            NSDictionary * aWaitingDownloadDict = self.waitingDownloadsArray[anIndex];
            if ([aWaitingDownloadDict[@"downloadToken"] isEqualToString:aDownloadIndentifier]) {
                aFoundIndex = anIndex;
                break;
            }
            aFoundIndex++;
        }
        if (aFoundIndex > -1)
        {
            [self.waitingDownloadsArray removeObjectAtIndex:aFoundIndex];
            
            NSError * aCancelledError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil];
            [self.fileDownloadDelegate downloadFailedWithIdentifier:aDownloadIndentifier error:aCancelledError httpStatusCode:0 errorMessagesStack:nil resumeData:nil];
            
            [self startNextWaitingDownload];
        }
    }
}

- (void)cancelDownloadWithDownloadID:(NSInteger)aDownloadID
{
    LTDownloadItem *aDownloadItem = [self.activeDownloadsDictionary objectForKey:@(aDownloadID)];
    if (aDownloadItem)
    {
        NSURLSessionDownloadTask *aDownloadTask = aDownloadItem.sessionDownloadTask;
        if (aDownloadTask)
        {
            [aDownloadTask cancel];
            //取消任务后 NSURLSessionTaskDelegate 会调用
            //URLSession:task:didCompleteWithError:
        }
        else
        {
            NSLog(@"INFO: NSURLSessionDownloadTask cancelled (task not found): %@ (%@, %d)", aDownloadItem.downloadToken, [NSString stringWithUTF8String:__FILE__].lastPathComponent, __LINE__);
            NSError *aCancelError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:nil];
            [self handleDownloadWithError:aCancelError downloadItem:aDownloadItem downloadID:aDownloadID resumaData:nil];
        }
    }
}

#pragma mark - Download Status


- (BOOL)isDownloadingIdentifier:(nonnull NSString *)aDownloadIdentifier
{
    BOOL isDownloading = NO;
    NSInteger aDownloadID = [self downloadIDForActiveDownloadToken:aDownloadIdentifier];
    if (aDownloadID > -1)
    {
        LTDownloadItem * aDownloadItem = [self.activeDownloadsDictionary objectForKey:@(aDownloadID)];
        if (aDownloadItem)
        {
            isDownloading = YES;
        }
    }
    if (isDownloading == NO)
    {
        for (NSDictionary *aWaitingDownloadDictionary in self.waitingDownloadsArray)
        {
            if ([aWaitingDownloadDictionary[@"downloadToken"] isEqualToString:aDownloadIdentifier]) {
                isDownloading = YES;
                break;
            }
        }
    }
    
    return isDownloading;
}

- (BOOL)isWaitingForDownloadOfIdentifier:(nonnull NSString *)aDownloadIdentifier
{
    BOOL isWaitingForDownload = NO;
    for (NSDictionary * aWaitDownloadDict in self.waitingDownloadsArray)
    {
        if ([aWaitDownloadDict[@"downloadToken"] isEqualToString:aDownloadIdentifier])
        {
            isWaitingForDownload = YES;
            break;
        }
    }
    NSInteger aDownloadID = [self downloadIDForActiveDownloadToken:aDownloadIdentifier];
    if (aDownloadID > -1)
    {
        LTDownloadItem *aDownloadItem = [self.activeDownloadsDictionary objectForKey:@(aDownloadID)];
        if (aDownloadItem && (aDownloadItem.receivedFileSizeInBytes == 0)) {
            isWaitingForDownload = YES;
        }
    }
    
    return isWaitingForDownload;
}

- (BOOL)hasActiveDownloads
{
    BOOL aHasActiveDownloadsFlag = NO;
    if (self.activeDownloadsDictionary.count > 0 || (self.waitingDownloadsArray.count > 0))
    {
        aHasActiveDownloadsFlag = YES;
    }
    return aHasActiveDownloadsFlag;
}

#pragma mark - BackgroundSessionCompletionHandlerBlock

- (void)setBackgroundSessionCompletionHandlerBlock:(nullable LTBackgroundSessionCompletionHandlerBlock)aBackgroundSessionCompletionHandlerBlock
{
    self.backgroundSessionCompletionHandlerBlock = aBackgroundSessionCompletionHandlerBlock;
}

#pragma mark - NSURLSession
#pragma mark - NSURLSessionDownloadDelegate

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    NSString *anErrorString = nil;
    LTDownloadItem *aDownloadItem = [self.activeDownloadsDictionary objectForKey:@(downloadTask.taskIdentifier)];
    if (aDownloadItem)
    {
        NSURL *aLocalDestinationFileURL = nil;
        if ([self.fileDownloadDelegate respondsToSelector:@selector(localFileURLForIdentifier:remoteURL:)]) {
            NSURL *aRemoteURL = [[downloadTask.originalRequest URL] copy];
            if (aRemoteURL)
            {
                aLocalDestinationFileURL = [self.fileDownloadDelegate localFileURLForIdentifier:aDownloadItem.downloadToken remoteURL:aRemoteURL];
            }
            else
            {
                anErrorString = [NSString stringWithFormat:@"ERROR : Missing information: Remote URL (token: %@)(%@, %d)",aDownloadItem.downloadToken,[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__];
                NSLog(@"%@",anErrorString);
            }
        }
        else
        {
            aLocalDestinationFileURL = [LTDownloadManager localFileURLForRemoteURL:downloadTask.originalRequest.URL];
        }
        
        if (aLocalDestinationFileURL)
        {
            if([[NSFileManager defaultManager] fileExistsAtPath:aLocalDestinationFileURL.path] == YES)
            {
                NSError *aRemoveError = nil;
                [[NSFileManager defaultManager] removeItemAtURL:aLocalDestinationFileURL error:&aRemoveError];
                if (aRemoveError)
                {
                    anErrorString = [NSString stringWithFormat:@"ERROR : Error on removing  file at %@: %@ (%@, %d)",aLocalDestinationFileURL, aRemoveError,[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__];
                }
            }
            //移动文件到指定目录
            NSError *anError = nil;
            BOOL aSuccessFlag = [[NSFileManager defaultManager] moveItemAtURL:location toURL:aLocalDestinationFileURL error:&anError];
            if (aSuccessFlag == NO)
            {
                NSError *aMoveError = anError;
                if (aMoveError == nil)
                {
                    aMoveError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorCannotMoveFile userInfo:nil];
                }
                anErrorString = [NSString stringWithFormat:@"ERROR: Unable to move file from %@ to %@ (%@) (%@, %d)",location,aLocalDestinationFileURL,aMoveError.localizedDescription,[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__];
                NSLog(@"%@",anErrorString);
            }
            else
            {
                NSError *anError = nil;
                NSDictionary *aFileAttributesDictionary = [[NSFileManager defaultManager] attributesOfItemAtPath:aLocalDestinationFileURL.path error:&anError];
                if (anError) {
                    anErrorString = [NSString stringWithFormat:@"ERR: Error on getting file size for item at %@: %@ (%@, %d)", aLocalDestinationFileURL, anError.localizedDescription, [NSString stringWithUTF8String:__FILE__].lastPathComponent, __LINE__];
                    NSLog(@"%@", anErrorString);
                }
                else
                {
                    unsigned long long aFileSize = [aFileAttributesDictionary fileSize];
                    if (aFileSize == 0)
                    {
                        NSError *aFileSizeZeroError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorZeroByteResource userInfo:nil];
                        anErrorString = [NSString stringWithFormat:@"ERR: Zero file size for item at %@: %@ (%@, %d)", aLocalDestinationFileURL, aFileSizeZeroError.localizedDescription, [NSString stringWithUTF8String:__FILE__].lastPathComponent, __LINE__];
                        NSLog(@"%@", anErrorString);
                    }
                    else
                    {
                        if ([self.fileDownloadDelegate respondsToSelector:@selector(downloadAtLocationFileURL:isValidForDownloadIdentifier:)])
                        {
                            BOOL anIsValidDownloadFlag = [self.fileDownloadDelegate downloadAtLocationFileURL:aLocalDestinationFileURL isValidForDownloadIdentifier:aDownloadItem.downloadToken];
                            if (anIsValidDownloadFlag == NO)
                            {
                                anErrorString = [NSString stringWithFormat:@"ERROR: Download check failed for item at %@",aLocalDestinationFileURL];
                            }
                        }
                    }
                }
            }
            
        }
        else
        {
            anErrorString = [NSString stringWithFormat:@"ERROR: Missing information: Lcal file URL (token:%@) (%@, %d)",aDownloadItem.downloadToken,[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__];
        }
        if (anErrorString)
        {
            NSMutableArray<NSString *> *anErrorMessageStackArray = [aDownloadItem.errorMessagesStack mutableCopy];
            if (anErrorMessageStackArray == nil)
            {
                anErrorMessageStackArray = [NSMutableArray array];
            }
            [anErrorMessageStackArray insertObject:anErrorString atIndex:0];
            [aDownloadItem setErrorMessagesStack:anErrorMessageStackArray];
        }
        else
        {
            aDownloadItem.finalLocalFileURL = aLocalDestinationFileURL;
        }
    }
    else
    {
        NSLog(@"ERR: Missing download item for taskIdentifier: %@ (%@, %d)", @(downloadTask.taskIdentifier), [NSString stringWithUTF8String:__FILE__].lastPathComponent, __LINE__);
    }
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    LTDownloadItem *aDownloadItem = [self.activeDownloadsDictionary objectForKey:@(downloadTask.taskIdentifier)];
    if (aDownloadItem)
    {
        if (aDownloadItem.downloadStartDate == nil) {
            aDownloadItem.downloadStartDate = [NSDate date];
        }
        aDownloadItem.receivedFileSizeInBytes = totalBytesWritten;
        aDownloadItem.expectedFileSizeInBytes = totalBytesExpectedToWrite;
        if ([self.fileDownloadDelegate respondsToSelector:@selector(downloadProgressForIdentifier:)])
        {
            NSString * aTaskDescription = [downloadTask.taskDescription copy];
            if (aTaskDescription)
            {
                [self.fileDownloadDelegate downloadProgressChangedForIdentifier:aTaskDescription];
            }
        }
    }
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
    LTDownloadItem *aDownloadItem = [self.activeDownloadsDictionary objectForKey:@(downloadTask.taskIdentifier)];
    if (aDownloadItem)
    {
        aDownloadItem.resumedFileSizeInBytes = fileOffset;
        aDownloadItem.downloadStartDate = [NSDate date];
        aDownloadItem.bytesPerSecondSpeed = 0;
        NSLog(@"INFO: Download (id: %@) resumed (offset: %@ bytes, expected: %@ bytes", downloadTask.taskDescription, @(fileOffset), @(expectedTotalBytes));
    }
}

#pragma mark - NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
    LTDownloadItem *aDownloadItem = [self.activeDownloadsDictionary objectForKey:@(task.taskIdentifier)];
    if (aDownloadItem)
    {
        NSHTTPURLResponse *aHTTPResponse = (NSHTTPURLResponse *)task.response;
        NSInteger aHTTPStatusCode = aHTTPResponse.statusCode;
        if (error == nil)
        {
            BOOL aHTTPStatusCodeIsCorrectFlag = NO;
            if ([self.fileDownloadDelegate respondsToSelector:@selector(httpStatusCode:isVailDownloadIdentifier:)]) {
                aHTTPStatusCodeIsCorrectFlag = [self.fileDownloadDelegate httpStatusCode:aHTTPStatusCode isVailDownloadIdentifier:aDownloadItem.downloadToken];
            }
            else
            {
                aHTTPStatusCodeIsCorrectFlag = [LTDownloadManager httpStatusCode:aHTTPStatusCode isValidForDownloadIdentifier:aDownloadItem.downloadToken];
            }
            
            if (aHTTPStatusCodeIsCorrectFlag == YES)
            {
                NSURL *aFileLocalFileURL = aDownloadItem.finalLocalFileURL;
                if (aFileLocalFileURL)
                {
                    [self handleSuccessFulDownloadToLocalFileURL:aFileLocalFileURL downloadItem:aDownloadItem downloadID:task.taskIdentifier];
                }
                else
                {
                    NSError *aFinalError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorResourceUnavailable userInfo:nil];
                    [self handleDownloadWithError:aFinalError downloadItem:aDownloadItem downloadID:task.taskIdentifier resumaData:nil];
                }
            }
            else
            {
                NSString * anErrorString = [NSString stringWithFormat:@"Invaild http status code: %@",@(aHTTPStatusCode)];
                NSMutableArray<NSString *> *anErrorMessagesStackArray = [aDownloadItem.errorMessagesStack mutableCopy];
                if (anErrorMessagesStackArray == nil)
                {
                    anErrorMessagesStackArray = [NSMutableArray array];
                }
                [anErrorMessagesStackArray insertObject:anErrorString atIndex:0];
                [aDownloadItem setErrorMessagesStack:anErrorMessagesStackArray];
                
                NSError *aFinalError = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorBadServerResponse userInfo:nil];
                [self handleDownloadWithError:aFinalError downloadItem:aDownloadItem downloadID:task.taskIdentifier resumaData:nil];
            }
        }
        else
        {
            NSData * aSessionDownloadTaskResumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
            [self handleDownloadWithError:error downloadItem:aDownloadItem downloadID:task.taskIdentifier resumaData:aSessionDownloadTaskResumeData];
        }
    }
    else
    {
        NSLog(@"ERROR: Download item not found for download task: %@ (%@, %d)",task,[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__);
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler
{
    
}

#pragma mark - NSURLSessionDelegate

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    if (self.backgroundSessionCompletionHandlerBlock)
    {
        void (^completionHandler)() = self.backgroundSessionCompletionHandlerBlock;
        self.backgroundSessionCompletionHandlerBlock = nil;
        completionHandler();
    }
}

- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error
{
    NSLog(@"ERROR:URL session did become invalid with error: %@(%@, %d)",
          error,[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__);
}


#pragma mark - LTDownloadManager Defaults


+ (nullable NSURL *)localFileURLForRemoteURL:(nonnull NSURL *)aRemoteURL
{
    NSURL *aLocalFileURL = nil;
    NSURL *aFileDownloadDirectoryURL = nil;
    NSError *anError = nil;
    NSArray * aDocumentDirectoryURLsArray = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
    NSURL *aDocumentDirectoryURL = [aDocumentDirectoryURLsArray firstObject];
    if (aDocumentDirectoryURL)
    {
        aFileDownloadDirectoryURL = [aDocumentDirectoryURL URLByAppendingPathComponent:@"file-download" isDirectory:YES];
        if ([[NSFileManager defaultManager] fileExistsAtPath:aFileDownloadDirectoryURL.path] == NO)
        {
            //create directory path for download file.
            BOOL aCreateDirectorySuccess = [[NSFileManager defaultManager] createDirectoryAtPath:aFileDownloadDirectoryURL.path withIntermediateDirectories:YES attributes:nil error:&anError];
            if (aCreateDirectorySuccess == NO)
            {
                NSLog(@"ERROR on creat directory: %@ (%@, %d)",anError,[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__);
            }
            else
            {
                BOOL aSetResourceValueSuccess = [aFileDownloadDirectoryURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&anError];
                if (aSetResourceValueSuccess == NO)
                {
                    NSLog(@"ERROR on set resource value (NSURLIsExcludedFromBackupKey): %@ (%@, %d)",anError,[NSString stringWithUTF8String:__FILE__].lastPathComponent,__LINE__);
                }
            }
            //user UUID create local file name.
            NSString * aLocalFileName = [NSString stringWithFormat:@"%@.%@",[[NSUUID UUID] UUIDString],[[aRemoteURL lastPathComponent] pathExtension]];
            
            aLocalFileURL = [aFileDownloadDirectoryURL URLByAppendingPathComponent:aLocalFileName isDirectory:NO];
        }
    }
    return aLocalFileURL;
}

+ (BOOL)httpStatusCode:(NSInteger)aHttpStatusCode isValidForDownloadIdentifier:(nonnull NSString *)aDownloadIdentifier
{
    BOOL anIsCorrectFlag = NO;
    if ((aHttpStatusCode >= 200) && (aHttpStatusCode < 300))
    {
        anIsCorrectFlag = YES;
    }
    return anIsCorrectFlag;
}

#pragma mark - Download Completion Handler

- (void)handleSuccessFulDownloadToLocalFileURL:(nonnull NSURL *)aLocalFileURL
                                  downloadItem:(nonnull LTDownloadItem *)aDownloadItem
                                    downloadID:(NSUInteger)aDownloadID
{
    [self startNextWaitingDownload];
}

- (void)handleDownloadWithError:(nonnull NSError *)anError
                   downloadItem:(nonnull LTDownloadItem *)aDownloadItem
                     downloadID:(NSUInteger)downloadID
                     resumaData:(nullable NSData *)aResumaData
{
    aDownloadItem.progress.completedUnitCount = aDownloadItem.progress.totalUnitCount;
    [self.activeDownloadsDictionary removeObjectForKey:@(downloadID)];
    [self.fileDownloadDelegate decrementNetworkActivityIndicatorActivityCount];
    [self.fileDownloadDelegate downloadFailedWithIdentifier:aDownloadItem.downloadToken
                                                      error:anError
                                             httpStatusCode:aDownloadItem.lastHttpStatusCode
                                         errorMessagesStack:aDownloadItem.errorMessagesStack
                                                 resumeData:aResumaData];
    
    [self startNextWaitingDownload];
}

#pragma mark - Download Progress

- (nullable LTDownloadProgress *)downloadProgressForIdentifier:(nonnull NSString *)aDownloadIdentifier
{
    LTDownloadProgress *aDownloadProgress = nil;
    NSInteger aDownloadID = [self downloadIDForActiveDownloadToken:aDownloadIdentifier];
    if (aDownloadID > -1)
    {
        aDownloadProgress = [self downloadProgressForDownloadID:aDownloadID];
    }
    return aDownloadProgress;
}

- (nullable LTDownloadProgress *)downloadProgressForDownloadID:(NSUInteger)aDownloadID
{
    LTDownloadProgress * aDownloadProgress = nil;
    LTDownloadItem *aDownloadItem = [self.activeDownloadsDictionary objectForKey:@(aDownloadID)];
    if(aDownloadItem)
    {
        float aDownloadProgressFloat = 0.0;
        if (aDownloadItem.expectedFileSizeInBytes > 0)
        {
            aDownloadProgressFloat = (float)aDownloadItem.receivedFileSizeInBytes/(float)aDownloadItem.expectedFileSizeInBytes;
        }
        NSDictionary *aRemainingTimeDict = [LTDownloadManager remainingTimeAndBytesPerSecondForDownloadItem:aDownloadItem];
        
        [aDownloadItem.progress setUserInfoObject:[aRemainingTimeDict objectForKey:@"remainingTime"] forKey:NSProgressEstimatedTimeRemainingKey];
        [aDownloadItem.progress setUserInfoObject:[aRemainingTimeDict objectForKey:@"bytesPerSecondSpeed"] forKey:NSProgressThroughputKey];
        
        aDownloadProgress = [[LTDownloadProgress alloc] initWithDownloadProgress:aDownloadProgressFloat expectedFileSize:aDownloadItem.expectedFileSizeInBytes receivedFileSize:aDownloadItem.receivedFileSizeInBytes estimatedRemainingTime:[[aRemainingTimeDict objectForKey:@"remainingTime"] doubleValue] bytesPerSecondSpeed:[[aRemainingTimeDict objectForKey:@"bytesPerSecondSpeed"] unsignedIntegerValue]  progress:aDownloadItem.progress];
    }
    return aDownloadProgress;
}

#pragma mark - Utilities

- (NSInteger)downloadIDForActiveDownloadToken:(nonnull NSString *)aDownloadToken
{
    NSInteger aFoundDownloadID = -1;
    NSArray *aDownloadKeysArray = [self.activeDownloadsDictionary allKeys];
    for (NSNumber * aDownloadID in aDownloadKeysArray) {
        LTDownloadItem * aDownloadItem = [self.activeDownloadsDictionary objectForKey:aDownloadID];
        if ([aDownloadItem.downloadToken isEqualToString:aDownloadToken]) {
            aFoundDownloadID = [aDownloadID unsignedIntegerValue];
            break;
        }
    }
    return aFoundDownloadID;
}

- (void)startNextWaitingDownload
{
    if((self.maxConcurrentFileDownloadsCount == -1) || ((NSInteger) self.activeDownloadsDictionary.count < self.maxConcurrentFileDownloadsCount))
    {
        if (self.waitingDownloadsArray.count > 0) {
            NSDictionary *aWaitingDownloadDict = [self.waitingDownloadsArray objectAtIndex:0];
            NSString *aDownloadToken = aWaitingDownloadDict[@"downloadToken"];
            NSURL *aRemoteURL = aWaitingDownloadDict[@"remoteURL"];
            NSData *aResumeData = aWaitingDownloadDict[@"resumeData"];
            [self.waitingDownloadsArray removeObjectAtIndex:0];
            [self startDownloadWithDownloadToken:aDownloadToken fromRemoteURL:aRemoteURL usingResumeData:aResumeData];
        }
    }
}

+ (nonnull NSDictionary *)remainingTimeAndBytesPerSecondForDownloadItem:(nonnull LTDownloadItem *)aDownloadItem
{
    NSTimeInterval aRemainingTimeInterval = 0.0;
    NSUInteger aBytesPerSecondsSpeed = 0;
    if ((aDownloadItem.receivedFileSizeInBytes > 0) && (aDownloadItem.expectedFileSizeInBytes > 0))
    {
        float aSmoothingFactor = 0.8;
        NSTimeInterval aDownloadDurationUntilNow = [[NSDate date] timeIntervalSinceDate:aDownloadItem.downloadStartDate];
        int64_t aDownloadFileSize = aDownloadItem.receivedFileSizeInBytes - aDownloadItem.resumedFileSizeInBytes;
        float aCurrentBytesPerSecondSpeed = (aDownloadDurationUntilNow > 0) ? (aDownloadFileSize / aDownloadDurationUntilNow) : 0.0;
        float aNewWeightedBytesPerSecondSpeed = 0.0;
        if (aDownloadItem.bytesPerSecondSpeed > 0.0)
        {
            aNewWeightedBytesPerSecondSpeed = (aSmoothingFactor * aCurrentBytesPerSecondSpeed) + ((1.0 - aSmoothingFactor) *(float)aDownloadItem.bytesPerSecondSpeed);
        }else
        {
            aNewWeightedBytesPerSecondSpeed = aCurrentBytesPerSecondSpeed;
        }
        if (aNewWeightedBytesPerSecondSpeed > 0.0)
        {
            aRemainingTimeInterval = (aDownloadItem.expectedFileSizeInBytes - aDownloadItem.resumedFileSizeInBytes - aDownloadFileSize) / aNewWeightedBytesPerSecondSpeed;
        }
        aBytesPerSecondsSpeed = (NSUInteger)aNewWeightedBytesPerSecondSpeed;
        aDownloadItem.bytesPerSecondSpeed = aBytesPerSecondsSpeed;
    }
    
    return @{@"bytesPerSecondSpeed":@(aBytesPerSecondsSpeed),@"remainingTime":@(aRemainingTimeInterval)};
}

#pragma mark - Description


- (NSString *)description
{
    NSMutableDictionary *aDescriptionDict = [NSMutableDictionary dictionary];
    [aDescriptionDict setObject:self.activeDownloadsDictionary forKey:@"activeDownloadsDictionary"];
    [aDescriptionDict setObject:self.waitingDownloadsArray forKey:@"waitingDownloadsArray"];
    [aDescriptionDict setObject:@(self.maxConcurrentFileDownloadsCount) forKey:@"maxConcurrentFileDownloadsCount"];
    
    NSString *aDescriptionString = [NSString stringWithFormat:@"%@", aDescriptionDict];
    
    return aDescriptionString;
}

@end
